package org.hsqldb.persist;

import java.io.IOException;

import org.hsqldb.ColumnSchema;
import org.hsqldb.Database;
import org.hsqldb.HsqlException;
import org.hsqldb.Row;
import org.hsqldb.RowAction;
import org.hsqldb.RowLinkList;
import org.hsqldb.Session;
import org.hsqldb.Table;
import org.hsqldb.TableBase;
import org.hsqldb.TransactionManager;
import org.hsqldb.error.Error;
import org.hsqldb.error.ErrorCode;
import org.hsqldb.index.Index;
import org.hsqldb.index.IndexLinkList;
import org.hsqldb.index.NodeLinkList;
import org.hsqldb.lib.ArrayUtil;
import org.hsqldb.navigator.RowIterator;
import org.hsqldb.rowio.RowInputInterface;
import org.hsqldb.rowio.RowOutputInterface;
import org.hsqldb.types.Type;

public class RowStoreLinkList implements PersistentStore{
	 	Session                   session;
	    Database                  database;
	    PersistentStoreCollection manager;
	    Index[]                   indexList    = Index.emptyArray;
	    CachedObject[]            accessorList = CachedObject.emptyArray;
	    TableBase                 table;
	    int                       elementCount;
	    
	    DataFileCache      cache;
	    RowOutputInterface rowOut;

	    // for result tables
	    // for INFORMATION SCHEMA tables
	    private long timestamp;

	    //
	    PersistentStore[] subStores = PersistentStore.emptyArray;

	    public RowStoreLinkList(){
	    	
	    }
	    public RowStoreLinkList(PersistentStoreCollection manager,
                DataFileCache cache, Table table) {

			this.database     = table.database;
			this.manager      = manager;
			this.table        = table;
			this.indexList    = table.getIndexList();
			this.accessorList = new CachedObject[indexList.length];
			this.cache        = cache;
			
			if (cache != null) {
			 rowOut = cache.rowOut.duplicate();
			 cache.adjustStoreCount(1);
			}
			manager.setStore(table, this);
		}
	    public TableBase getTable() {
	        return table;
	    }

	    public long getTimestamp() {
	        return timestamp;
	    }

	    public void setTimestamp(long timestamp) {
	        this.timestamp = timestamp;
	    }

	    public boolean isMemory() {
	        return false;
	    }

	    public void setMemory(boolean mode) {}

	    public int getAccessCount() {
	    	return cache.getAccessCount();
		}

	    public void set(CachedObject object) {
	    	
	    	Row row = ((Row) object);

	        database.txManager.setTransactionInfo(row);
		}
	    public CachedObject get(int key) {

	        CachedObject object = cache.get(key, this, false);

	        return object;
	    }
	    public CachedObject get(int key, boolean keep) {
	    	CachedObject object = cache.get(key, this, keep);

	        return object;
		}

	    public CachedObject get(CachedObject object, boolean keep) {
	    	object = cache.get(object, this, keep);

	        return object;
		}

	    public int getStorageSize(int key) {
	    	return cache.get(key, this, false).getStorageSize();
		}

	    public void add(CachedObject object) {
	    	int size = object.getRealSize(rowOut);

	        size += indexList.length * NodeLinkList.SIZE_IN_BYTE;
	        size = rowOut.getStorageSize(size);

	        object.setStorageSize(size);
	        cache.add(object);
		}

	    public CachedObject get(RowInputInterface in) {
	    	try {
	            return new RowLinkList(table, in);
	        } catch (IOException e) {
	            throw Error.error(ErrorCode.DATA_FILE_ERROR, e);
	        }
		}

	    public CachedObject getNewInstance(int size) {
			return null;
		}

	    public CachedObject getNewCachedObject(Session session,
	            Object object, boolean tx) {
	    	Row row = new RowLinkList(table, (Object[]) object, this);

	        add(row);

	        if (tx) {
	            RowAction action = new RowAction(session, table,
	                                             RowAction.ACTION_INSERT, row,
	                                             null);

	            row.rowAction = action;
	        }

	        return row;
		}
	    
	    public void removePersistence(int i) {
	    	
		}

	    public void removeAll() {
	    	elementCount = 0;

	        ArrayUtil.fillArray(accessorList, null);
		}
	    
	    public void commitRow(Session session, Row row, int changeAction,
                int txModel) {
		Object[] data = row.getData();
		
		switch (changeAction) {
		
		  case RowAction.ACTION_DELETE :
		      database.logger.writeDeleteStatement(session, (Table) table,
		                                           data);
		
		      if (txModel == TransactionManager.LOCKS) {
		          remove(row.getPos());
		      }
		      break;
		
		  case RowAction.ACTION_INSERT :
		      database.logger.writeInsertStatement(session, row,
		                                           (Table) table);
		      break;
		
		  case RowAction.ACTION_INSERT_DELETE :
		
		      // INSERT + DELETE
		      if (txModel == TransactionManager.LOCKS) {
		          remove(row.getPos());
		      }
		      break;
		
		  case RowAction.ACTION_DELETE_FINAL :
		      delete(session, row);
		
		      // remove info after delete
		      database.txManager.removeTransactionInfo(row);
		      remove(row.getPos());
		      break;
			}
	    }
	    
	    public void remove(int i) {
	    	cache.remove(i, this);
		}

	    
	    public void release(int i) {
	    	cache.release(i);
		}

	    public void commitPersistence(CachedObject object) {
		}

	    public DataFileCache getCache() {
	    	return cache;
		}

	    public void setCache(DataFileCache cache) {
	    	this.cache = cache;
		}
/*
	    public void release() {
	    	ArrayUtil.fillArray(accessorList, null);
	        cache.adjustStoreCount(-1);

	        cache = null;
		}*/
	    public void release() {

	        ArrayUtil.fillArray(accessorList, null);
	        table.database.logger.closeTextCache((Table) table);

	        cache = null;
	    }
	    public PersistentStore getAccessorStore(Index index) {
	        return null;
	    }

	    public CachedObject getAccessor(Index key) {

	    	NodeLinkList node = (NodeLinkList) accessorList[key.getPosition()];

	        if (node == null) {
	            return null;
	        }
	        if (!node.isInMemory()) {
	            RowLinkList row = (RowLinkList) get(node.getPos(), false);

	            node                            = row.getNode(key.getPosition());
	            accessorList[key.getPosition()] = node;
	        }
	        return node;
	    }
	    public void setAccessor(Index key, CachedObject accessor) {

	        Index index = (Index) key;

	        accessorList[index.getPosition()] = accessor;
	    }
	    
	    public void setAccessor(Index key, int accessor) {

	        CachedObject object = get(accessor, false);

	        if (object != null) {
	            NodeLinkList node = ((RowLinkList) object).getNode(key.getPosition());

	            object = node;
	        }

	        setAccessor(key, object);
	    }
	    /*
	    public void resetAccessorKeys(Index[] keys) {

	        if (indexList.length == 0 || accessorList[0] == null) {
	            indexList    = keys;
	            accessorList = new CachedObject[indexList.length];

	            return;
	        }

	        throw Error.runtimeError(ErrorCode.U_S0500, "RowStoreAVLDisk");
	    }*/
	    public void writeLock() {
	        cache.writeLock.lock();
	    }

	    public void writeUnlock() {
	        cache.writeLock.unlock();
	    }
	    /**
	     * Basic delete with no logging or referential checks.
	     */
	    public void delete(Session session, Row row) {

	        row = (Row) get(row, false);

	        for (int i = 0; i < indexList.length; i++) {
	            indexList[i].delete(session, this, row);
	        }

	        for (int i = 0; i < subStores.length; i++) {
	            subStores[i].delete(session, row);
	        }

	        row.delete(this);

	        elementCount--;
	    }

	    public void indexRow(Session session, Row row) {

	        int i = 0;

	        try {
	            for (; i < indexList.length; i++) {
	                indexList[i].insert(session, this, row);
	            }

	            int j = 0;

	            try {
	                for (j = 0; j < subStores.length; j++) {
	                    subStores[j].indexRow(session, row);
	                }
	            } catch (HsqlException e) {

	                // unique index violation - rollback insert
	                int count = j;

	                j = 0;

	                for (; j < count; j++) {
	                    subStores[j].delete(session, row);
	                }

	                throw e;
	            }

	            elementCount++;
	        } catch (HsqlException e) {
	            int count = i;

	            i = 0;

	            // unique index violation - rollback insert
	            for (; i < count; i++) {
	                indexList[i].delete(session, this, row);
	            }

	            remove(row.getPos());

	            throw e;
	        }
	    }

	    //
	    public final void indexRows(Session session) {

	        for (int i = 1; i < indexList.length; i++) {
	            setAccessor(indexList[i], null);
	        }

	        RowIterator it = rowIterator();

	        while (it.hasNext()) {
	            Row row = it.getNextRow();

	            ((RowLinkList) row).clearNonPrimaryNodes();

	            for (int i = 1; i < indexList.length; i++) {
	                indexList[i].insert(session, this, row);
	            }
	        }
	    }

	    public final RowIterator rowIterator() {

	        Index index = indexList[0];

	        for (int i = 0; i < indexList.length; i++) {
	            if (indexList[i].isClustered()) {
	                index = indexList[i];

	                break;
	            }
	        }

	        return index.firstRow(this);
	    }

	    public void resetAccessorKeys(Index[] keys) {

	        Index[] oldIndexList = indexList;

	        if (indexList.length == 0 || accessorList[0] == null) {
	            indexList    = keys;
	            accessorList = new CachedObject[indexList.length];

	            return;
	        }

	        // method might be called twice
	        if (indexList == keys) {
	            return;
	        }

	        CachedObject[] oldAccessors = accessorList;
	        int            limit        = indexList.length;
	        int            diff         = keys.length - indexList.length;
	        int            position     = 0;

	        if (diff < -1) {
	            throw Error.runtimeError(ErrorCode.U_S0500, "RowStoreLinkList");
	        } else if (diff == -1) {
	            limit = keys.length;
	        } else if (diff == 0) {
	            throw Error.runtimeError(ErrorCode.U_S0500, "RowStoreLinklist");
	        } else if (diff == 1) {
	            ;
	        } else {
	            for (; position < limit; position++) {
	                if (indexList[position] != keys[position]) {
	                    break;
	                }
	            }

	            Index[] tempKeys = (Index[]) ArrayUtil.toAdjustedArray(indexList,
	                null, position, 1);

	            tempKeys[position] = keys[position];

	            resetAccessorKeys(tempKeys);
	            resetAccessorKeys(keys);

	            return;
	        }

	        for (; position < limit; position++) {
	            if (indexList[position] != keys[position]) {
	                break;
	            }
	        }

	        accessorList = (CachedObject[]) ArrayUtil.toAdjustedArray(accessorList,
	                null, position, diff);
	        indexList = keys;

	        try {
	            if (diff > 0) {
	                insertIndexNodes(indexList[0], indexList[position]);
	            } else {
	                dropIndexFromRows(indexList[0], oldIndexList[position]);
	            }
	        } catch (HsqlException e) {
	            accessorList = oldAccessors;
	            indexList    = oldIndexList;

	            throw e;
	        }
	    }

	    public Index[] getAccessorKeys() {
	        return indexList;
	    }

	    public int elementCount() {

	        Index index = this.indexList[0];

	        if (elementCount < 0) {
	            elementCount = ((IndexLinkList) index).getNodeCount(session, this);
	        }

	        return elementCount;
	    }

	    public int elementCount(Session session) {

	        Index index = this.indexList[0];

	        if (elementCount < 0) {
	            elementCount = ((IndexLinkList) index).getNodeCount(session, this);
	        }

	        if (session != null) {
	            int txControl = session.database.txManager.getTransactionControl();

	            if (txControl != TransactionManager.LOCKS) {
	                switch (table.getTableType()) {

	                    case TableBase.MEMORY_TABLE :
	                    case TableBase.CACHED_TABLE :
	                    case TableBase.TEXT_TABLE :
	                        return ((IndexLinkList) index).getNodeCount(session, this);

	                    default :
	                }
	            }
	        }

	        return elementCount;
	    }

	    public int elementCountUnique(Index index) {
	        return 0;
	    }

	    public void setElementCount(Index key, int size, int uniqueSize) {
	        elementCount = size;
	    }

	    /**
	     * Moves the data from an old store to new after changes to table
	     * The colindex argument is the index of the column that was
	     * added or removed. The adjust argument is {-1 | 0 | +1}
	     */
	    public final void moveData(Session session, PersistentStore other,
	                               int colindex, int adjust) {

	        Type   oldtype  = null;
	        Type   newtype  = null;
	        Object colvalue = null;

	        if (adjust >= 0 && colindex != -1) {
	            ColumnSchema column = ((Table) table).getColumn(colindex);

	            colvalue = column.getDefaultValue(session);
	            newtype  = ((Table) table).getColumnTypes()[colindex];
	        }

	        if (adjust <= 0 && colindex != -1) {
	            oldtype = ((Table) other.getTable()).getColumnTypes()[colindex];
	        }

	        try {
	            Table       table = (Table) this.table;
	            RowIterator it    = other.rowIterator();

	            while (it.hasNext()) {
	                Row      row      = it.getNextRow();
	                Object[] olddata  = row.getData();
	                Object[] data     = table.getEmptyRowData();
	                Object   oldvalue = null;

	                if (adjust == 0 && colindex != -1) {
	                    oldvalue = olddata[colindex];
	                    colvalue = newtype.convertToType(session, oldvalue,
	                                                     oldtype);
	                }

	                ArrayUtil.copyAdjustArray(olddata, data, colvalue, colindex,
	                                          adjust);
	                table.systemSetIdentityColumn(session, data);

	                if (table.hasGeneratedColumn()) {
	                    ((Table) table).setGeneratedColumns(session, data);
	                }

	                table.enforceTypeLimits(session, data);
	                table.enforceRowConstraints(session, data);

	                // get object without RowAction
	                Row newrow = (Row) getNewCachedObject(session, data, false);

	                indexRow(session, newrow);
	            }

	            if (table.isTemp()) {
	                return;
	            }

	            if (oldtype != null && oldtype.isLobType()) {
	                it = other.rowIterator();

	                while (it.hasNext()) {
	                    Row      row      = it.getNextRow();
	                    Object[] olddata  = row.getData();
	                    Object   oldvalue = olddata[colindex];

	                    if (oldvalue != null) {
	                        session.sessionData.adjustLobUsageCount(oldvalue, -1);
	                    }
	                }
	            }

	            if (newtype != null && newtype.isLobType()) {
	                it = rowIterator();

	                while (it.hasNext()) {
	                    Row      row   = it.getNextRow();
	                    Object[] data  = row.getData();
	                    Object   value = data[colindex];

	                    if (value != null) {
	                        session.sessionData.adjustLobUsageCount(value, +1);
	                    }
	                }
	            }
	        } catch (java.lang.OutOfMemoryError e) {
	            throw Error.error(ErrorCode.OUT_OF_MEMORY);
	        }
	    }

	    public void reindex(Session session, Index index) {

	        setAccessor(index, null);

	        RowIterator it = table.rowIterator(this);

	        while (it.hasNext()) {
	            RowLinkList row = (RowLinkList) it.getNextRow();

	            row.getNode(index.getPosition()).delete();
	            index.insert(session, this, row);
	        }
	    }



	    void dropIndexFromRows(Index primaryIndex, Index oldIndex) {

	        RowIterator it       = primaryIndex.firstRow(this);
	        int         position = oldIndex.getPosition() - 1;

	        while (it.hasNext()) {
	            Row     row      = it.getNextRow();
	            int     i        = position - 1;
	            NodeLinkList backnode = ((RowLinkList) row).getNode(0);

	            while (i-- > 0) {
	                backnode = backnode.nodeNextIndex;
	            }

	            backnode.nodeNextIndex = backnode.nodeNextIndex.nodeNextIndex;
	        }
	    }

	    boolean insertIndexNodes(Index primaryIndex, Index newIndex) {

	        int           position = newIndex.getPosition();
	        RowIterator   it       = primaryIndex.firstRow(this);
	        int           rowCount = 0;
	        HsqlException error    = null;

	        try {
	            while (it.hasNext()) {
	                Row row = it.getNextRow();

	                ((RowLinkList) row).insertNode(position);

	                // count before inserting
	                rowCount++;

	                newIndex.insert(session, this, row);
	            }

	            return true;
	        } catch (java.lang.OutOfMemoryError e) {
	            error = Error.error(ErrorCode.OUT_OF_MEMORY);
	        } catch (HsqlException e) {
	            error = e;
	        }

	        // backtrack on error
	        // rowCount rows have been modified
	        it = primaryIndex.firstRow(this);

	        for (int i = 0; i < rowCount; i++) {
	            Row     row      = it.getNextRow();
	            NodeLinkList backnode = ((RowLinkList) row).getNode(0);
	            int     j        = position;

	            while (--j > 0) {
	                backnode = backnode.nodeNextIndex;
	            }

	            backnode.nodeNextIndex = backnode.nodeNextIndex.nodeNextIndex;
	        }

	        throw error;
	    }



		@Override
		public void rollbackRow(Session session, Row row, int changeAction,
				int txModel) {
			switch (changeAction) {

            case RowAction.ACTION_DELETE :
                if (txModel == TransactionManager.LOCKS) {
                    row = (Row) get(row, true);

                    ((RowLinkList) row).setNewNodes(this);
                    row.keepInMemory(false);
                    indexRow(session, row);
                }
                break;

            case RowAction.ACTION_INSERT :
                if (txModel == TransactionManager.LOCKS) {
                    delete(session, row);
                    remove(row.getPos());
                }
                break;

            case RowAction.ACTION_INSERT_DELETE :

                // INSERT + DELETE
                if (txModel == TransactionManager.LOCKS) {
                    remove(row.getPos());
                }
                break;
			}
			switch (changeAction) {

            case RowAction.ACTION_DELETE :
                if (txModel == TransactionManager.LOCKS) {
                    row = (Row) get(row, true);

                    ((RowLinkList) row).setNewNodes(this);
                    row.keepInMemory(false);
                    indexRow(session, row);
                }
                break;

            case RowAction.ACTION_INSERT :
                if (txModel == TransactionManager.LOCKS) {
                    delete(session, row);
                    remove(row.getPos());
                }
                break;

            case RowAction.ACTION_INSERT_DELETE :

                // INSERT + DELETE
                if (txModel == TransactionManager.LOCKS) {
                    remove(row.getPos());
                }
                break;
        }
		}
}
